// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jshin

#pragma once

#include <string>
#include <string_view>

#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"

namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using namespace std::literals;

class HuobiFeedUpdateParser {
 public:
  static void ParseBbo(BookBuilder* book_builder_, const rapidjson::Document& doc) {
    auto ts = stoull(doc["ts"].GetString()) * 1'000'000LL;  // ms -> ns
    auto tx = book_builder_->NewTransaction(ts);
    tx.Clear();

    const auto& tick = doc["tick"];
    if (tick.HasMember("ask") && (!tick["ask"].IsNull())) {
      tx.UpdateAsk0(
          stod(tick["ask"].GetString()),
          stod(tick["askSize"].GetString()));
    }

    if (tick.HasMember("bid") && (!tick["bid"].IsNull())) {
      tx.UpdateBid0(
          stod(tick["bid"].GetString()),
          stod(tick["bidSize"].GetString()));
    }
  }
  static void ParseBook(
      BookBuilder* book_builder_,
      const rapidjson::Document& doc,
      bool is_snapshot) {
    auto ts = stoull(doc["ts"].GetString()) * 1'000'000LL;  // ms -> ns
    auto tx = book_builder_->NewTransaction(ts);
    const rapidjson::Value& tick = doc["tick"];

    if (is_snapshot) {
      tx.Clear();
    }

    // Huobi sends a msg without 'asks', 'bids' field around rollover time
    if (tick.HasMember("asks") && (!tick["asks"].IsNull())) {
      for (const auto& pq : tick["asks"].GetArray()) {
        tx.UpdateAsk(
            stod(pq[0].GetString()),
            stod(pq[1].GetString()));
      }
    }
    if (tick.HasMember("bids") && (!tick["bids"].IsNull())) {
      for (const auto& pq : tick["bids"].GetArray()) {
        tx.UpdateBid(
            stod(pq[0].GetString()),
            stod(pq[1].GetString()));
      }
    }
  }
  static void ParseSnapshot(
      BookBuilder* book_builder_,
      const rapidjson::Document& doc,
      int64_t ts) {
    auto tx = book_builder_->NewTransaction(ts);
    tx.Clear();

    const rapidjson::Value& data = doc["data"];
    for (const auto& pq : data["asks"].GetArray()) {
      tx.UpdateAsk(
          stod(pq[0].GetString()),
          stod(pq[1].GetString()));
    }
    for (const auto& pq : data["bids"].GetArray()) {
      tx.UpdateBid(
          stod(pq[0].GetString()),
          stod(pq[1].GetString()));
    }
  }
  static void ParseTrade(const rapidjson::Value& trade_json, int qty_divisor, Trade* trade) {
    // trade_json->timestamp is reused
    trade->price = stod(trade_json["price"].GetString());
    trade->fill_qty = stod(trade_json["amount"].GetString()) / qty_divisor;
    trade->side = ParseTradeSide(trade_json["direction"].GetString());
  }
  static coin2::exchange::feed::TradeSide ParseTradeSide(std::string_view side) {
    if (side == "buy") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "sell") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else {
      throw std::runtime_error("[HuobiCommon] unknown side: " + std::string(side));
    }
  }
  static void ParseNav(const rapidjson::Document& doc, Nav* nav) {
    nav->Clear();
    auto ts = stoull(doc["ts"].GetString()) * 1'000'000LL;  // ms -> ns
    nav->timestamp = ts;
    const auto& tick = doc["tick"];
    nav->actual_leverage = stod(tick["actualLeverage"].GetString());
    nav->nav = stod(tick["nav"].GetString());
    nav->outstanding = stod(tick["outstanding"].GetString());

    for (const auto& ua : tick["basket"].GetArray()) {
      nav->basket.emplace_back(ua["currency"].GetString(), stod(ua["amount"].GetString()));
    }
  }

  static bool ParseKline(
      Klines* klines,
      const rapidjson::Value& obj,
      fastfeed::proto::KlineInterval interval,
      int64_t ts) {
    int64_t open_timestamp = stoull(obj["id"].GetString()) * 1'000'000'000LL;
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();

    // huobi publish unfreezed kline, but we only publish freezed kline
    HuobiFeedUpdateParser::Parse1Kline(kline, obj, interval, ts, open_timestamp);
    return freezed;
  }

  static void Parse1Kline(
      Kline* kline,
      const rapidjson::Value& obj,
      fastfeed::proto::KlineInterval interval,
      int64_t ts,
      int64_t open_timestamp) {
    kline->open_timestamp = open_timestamp;
    kline->open = stod(obj["open"].GetString());
    kline->high = stod(obj["high"].GetString());
    kline->low = stod(obj["low"].GetString());
    kline->close = stod(obj["close"].GetString());
    kline->volume = stod(obj["amount"].GetString());
    kline->turnover = stod(obj["vol"].GetString());
    kline->timestamp = ts;
    kline->interval = interval;
  }

  static fastfeed::proto::KlineInterval ParseKlineInterval(std::string_view period) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"1min", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"5min", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"15min", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"30min", fastfeed::proto::KLINE_INTERVAL_30MIN},
      {"60min", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"4hour", fastfeed::proto::KLINE_INTERVAL_4HOUR},
      {"1day", fastfeed::proto::KLINE_INTERVAL_1DAY},
      {"1mon", fastfeed::proto::KLINE_INTERVAL_1MON}
    };
    auto iter = interval_map.find(std::string(period));
    if (iter == interval_map.end()) {
      NOTREACHED() << "Not support interval " << period;
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }
};
}  // namespace impl

using impl::HuobiFeedUpdateParser;
